linux 键盘按键频率统计
·
Table of Contents
按键频率(key frequency)
统计 Linux 下自己实际敲击的 按键频率(key frequency),从而为 键盘布局优化(如重映射高频键到更符合人体工学的位置)提供数据支持
常用的监控按键的工具
- wev(wayland)
- 通过wayland(libinput)读取eventX(evdev)
- libinput list-devices
- apt install libinput-tools
- xev(x11)
- 通过x11(xkb)读取eventX
- xinput(x11)
- evtest
- apt install evtest
- 直接读取eventX
- showkey
- 通过内核tty系统读物eventX
- 只能在 VT(Ctrl+Alt+Fx)
- 看的是 内核 tty keycode / scancode
- uinput
- 创建虚拟输入设备/dev/intput/eventY
- /dev/uinput(虚拟设备注入点)
- /dev/input/eventX
- 内核输入子系统 (evdev)
- 键盘映射系统级修改:修改内核键码映射(setkeycodes)或控制台映射(loadkeys)
使用 evtest + 自定义脚本(推荐,安全、底层、不依赖 GUI)
- 所有数据仅保存在 ~/.local/share/keyfreq/,不记录键值内容(如不区分 a/A,只记录 KEY_A)。
- 无法恢复原始文本,仅统计物理按键频率,不构成键盘记录器(keylogger)
sudo apt install evtest # Debian/Ubuntu
sudo evtest
sudo apt install python3-pip
sudo usermod -aG input $USER
# 重新登录使组生效
pip3 install --user evdev
# 目录结构
# ~/.local/bin/keyfreqd
# ~/.config/systemd/user/keyfreqd.service
# ~/.local/share/keyfreq/ # 日志目录
#!/usr/bin/env python3
import sys
import struct
import os
from collections import Counter
# 从 evtest 或 /usr/include/linux/input-event-codes.h 获取 keycode 映射
KEY_NAMES = {
1: 'ESC', 2: '1', 3: '2', 4: '3', 5: '4', 6: '5', 7: '6', 8: '7', 9: '8', 10: '9', 11: '0',
12: 'MINUS', 13: 'EQUAL', 14: 'BACKSPACE',
15: 'TAB', 16: 'Q', 17: 'W', 18: 'E', 19: 'R', 20: 'T', 21: 'Y', 22: 'U', 23: 'I', 24: 'O', 25: 'P',
26: 'LEFTBRACE', 27: 'RIGHTBRACE', 28: 'ENTER',
29: 'LEFTCTRL', 30: 'A', 31: 'S', 32: 'D', 33: 'F', 34: 'G', 35: 'H', 36: 'J', 37: 'K', 38: 'L',
39: 'SEMICOLON', 40: 'APOSTROPHE', 41: 'GRAVE',
42: 'LEFTSHIFT', 43: 'BACKSLASH', 44: 'Z', 45: 'X', 46: 'C', 47: 'V', 48: 'B', 49: 'N', 50: 'M',
51: 'COMMA', 52: 'DOT', 53: 'SLASH', 54: 'RIGHTSHIFT',
56: 'LEFTALT', 57: 'SPACE', 58: 'CAPSLOCK',
97: 'RIGHTCTRL', 100: 'RIGHTALT',
# 方向键等
103: 'UP', 105: 'LEFT', 106: 'RIGHT', 108: 'DOWN',
}
def main(device_path):
counter = Counter()
with open(device_path, 'rb') as f:
while True:
data = f.read(24) # input_event 结构体大小
if not data:
break
tv_sec, tv_usec, type_, code, value = struct.unpack('llHHi', data)
if type_ == 1 and value == 1: # EV_KEY 且按下(非释放)
key_name = KEY_NAMES.get(code, f'KEY_{code}')
counter[key_name] += 1
print(f"\r{len(counter)} keys recorded...", end='', flush=True)
return counter
if __name__ == '__main__':
if len(sys.argv) != 2:
print("Usage: sudo python3 keyfreq.py /dev/input/eventX")
sys.exit(1)
device = sys.argv[1]
print(f"Recording key presses from {device} (Press Ctrl+C to stop)...")
try:
counter = main(device)
except KeyboardInterrupt:
print("\nStopped.")
# 输出排序结果
print("\n=== Key Frequency ===")
for key, count in counter.most_common():
print(f"{key:12} : {count}")
#!/usr/bin/env python3
# ~/.local/bin/keyfreqd
import os
import json
import time
from datetime import datetime, date
from collections import defaultdict
from pathlib import Path
from evdev import InputDevice, categorize, ecodes, list_devices
# 配置
LOG_DIR = Path.home() / ".local/share/keyfreq"
LOG_DIR.mkdir(parents=True, exist_ok=True)
# 过滤出键盘设备(基于名称或 capability)
def is_keyboard(dev):
if not dev.capabilities().get(ecodes.EV_KEY):
return False
name = dev.name.lower()
# 常见键盘关键词
keywords = ['keyboard', 'keypad', 'at keyboard', 'input device']
return any(kw in name for kw in keywords) or 'kbd' in name
def main():
# 获取所有输入设备
devices = [InputDevice(fn) for fn in list_devices()]
keyboards = [dev for dev in devices if is_keyboard(dev)]
if not keyboards:
print("No keyboard devices found. Check permissions (add user to 'input' group).", file=os.sys.stderr)
return
print(f"Monitoring {len(keyboards)} keyboard(s): {[d.name for d in keyboards]}")
# 当天的计数器
today = date.today()
counter = defaultdict(int)
try:
while True:
# 检查日期是否变更
new_day = date.today()
if new_day != today:
# 保存旧数据
save_day_log(today, counter)
# 重置
today = new_day
counter = defaultdict(int)
# 读取事件(非阻塞轮询)
for dev in keyboards:
try:
for event in dev.read():
if event.type == ecodes.EV_KEY:
key_event = categorize(event)
if hasattr(key_event, 'keycode'):
# 只记录按下(避免重复计数)
if event.value == 1: # key press
counter[key_event.keycode] += 1
except BlockingIOError:
continue # 无事件可读
except OSError as e:
if e.errno == 19: # 设备被拔出
keyboards = [d for d in keyboards if d != dev]
print(f"Device removed: {dev.name}", file=os.sys.stderr)
else:
raise
time.sleep(0.01) # 减少 CPU 占用
except KeyboardInterrupt:
save_day_log(today, counter)
print("\nExiting and saving final log.")
def save_day_log(day: date, counter: dict):
if not counter:
return
log_file = LOG_DIR / f"{day.isoformat()}.json"
# 合并已有数据(以防重复运行)
if log_file.exists():
with open(log_file, 'r') as f:
existing = json.load(f)
for k, v in counter.items():
existing[k] = existing.get(k, 0) + v
counter = existing
with open(log_file, 'w') as f:
json.dump(dict(counter), f, indent=2, sort_keys=True)
print(f"Saved {sum(counter.values())} keystrokes to {log_file}")
if __name__ == "__main__":
main()
- systemd 用户服务
cat ~/.config/systemd/user/keyfreqd.service
systemctl --user daemon-reload
systemctl --user enable --now keyfreqd.service
服务配置
[Unit]
Description=Keyboard Key Frequency Logger
After=graphical-session.target
[Service]
ExecStart=%h/.local/bin/keyfreqd
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
分析脚本
#!/usr/bin/env python3
# ~/.local/bin/keyfreq-analyze
import json
import sys
from pathlib import Path
from collections import defaultdict
LOG_DIR = Path.home() / ".local/share/keyfreq"
def main(days=7):
total = defaultdict(int)
count = 0
for log in sorted(LOG_DIR.glob("*.json"))[-days:]:
with open(log) as f:
data = json.load(f)
for k, v in data.items():
total[k] += v
count += 1
if not total:
print("No data found.")
return
print(f"Top 30 keys in last {count} day(s):")
print("-" * 40)
for key, freq in sorted(total.items(), key=lambda x: -x[1])[:30]:
print(f"{key:15} : {freq:>8}")
if __name__ == "__main__":
days = int(sys.argv[1]) if len(sys.argv) > 1 else 7
main(days)